// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
import 'package:in_app_purchase_storekit/in_app_purchase_storekit.dart';
import 'package:in_app_purchase_storekit/store_kit_2_wrappers.dart';

import 'consumable_store.dart';
import 'example_payment_queue_delegate.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  // When using the StoreKit plugin directly it is mandatory to register
  // the plugin as default instance as part of initializing the app.
  InAppPurchaseStoreKitPlatform.registerPlatform();

  runApp(_MyApp());
}

const String _kConsumableId = 'consumable';
const String _kUpgradeId = 'upgrade';
const String _kSilverSubscriptionId = 'subscription_silver';
const String _kGoldSubscriptionId = 'subscription_gold';
const List<String> _kProductIds = <String>[
  _kConsumableId,
  _kUpgradeId,
  _kSilverSubscriptionId,
  _kGoldSubscriptionId,
];

class _MyApp extends StatefulWidget {
  @override
  State<_MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<_MyApp> {
  final InAppPurchaseStoreKitPlatform _iapStoreKitPlatform =
      InAppPurchasePlatform.instance as InAppPurchaseStoreKitPlatform;
  final InAppPurchaseStoreKitPlatformAddition _iapStoreKitPlatformAddition =
      InAppPurchasePlatformAddition.instance!
          as InAppPurchaseStoreKitPlatformAddition;
  late StreamSubscription<List<PurchaseDetails>> _subscription;
  List<String> _notFoundIds = <String>[];
  List<ProductDetails> _products = <ProductDetails>[];
  List<PurchaseDetails> _purchases = <PurchaseDetails>[];
  List<String> _consumables = <String>[];
  bool _isAvailable = false;
  bool _purchasePending = false;
  bool _loading = true;
  String? _queryProductError;

  @override
  void initState() {
    final Stream<List<PurchaseDetails>> purchaseUpdated =
        _iapStoreKitPlatform.purchaseStream;
    _subscription = purchaseUpdated.listen(
      (List<PurchaseDetails> purchaseDetailsList) {
        _listenToPurchaseUpdated(purchaseDetailsList);
      },
      onDone: () {
        _subscription.cancel();
      },
      onError: (Object error) {
        // handle error here.
      },
    );

    // Register the example payment queue delegate
    _iapStoreKitPlatformAddition.setDelegate(ExamplePaymentQueueDelegate());

    initStoreInfo();
    super.initState();
  }

  Future<void> initStoreInfo() async {
    final bool isAvailable = await _iapStoreKitPlatform.isAvailable();
    if (!isAvailable) {
      setState(() {
        _isAvailable = isAvailable;
        _products = <ProductDetails>[];
        _purchases = <PurchaseDetails>[];
        _notFoundIds = <String>[];
        _consumables = <String>[];
        _purchasePending = false;
        _loading = false;
      });
      return;
    }

    final ProductDetailsResponse productDetailResponse =
        await _iapStoreKitPlatform.queryProductDetails(_kProductIds.toSet());
    if (productDetailResponse.error != null) {
      setState(() {
        _queryProductError = productDetailResponse.error!.message;
        _isAvailable = isAvailable;
        _products = productDetailResponse.productDetails;
        _purchases = <PurchaseDetails>[];
        _notFoundIds = productDetailResponse.notFoundIDs;
        _consumables = <String>[];
        _purchasePending = false;
        _loading = false;
      });
      return;
    }

    if (productDetailResponse.productDetails.isEmpty) {
      setState(() {
        _queryProductError = null;
        _isAvailable = isAvailable;
        _products = productDetailResponse.productDetails;
        _purchases = <PurchaseDetails>[];
        _notFoundIds = productDetailResponse.notFoundIDs;
        _consumables = <String>[];
        _purchasePending = false;
        _loading = false;
      });
      return;
    }

    final List<String> consumables = await ConsumableStore.load();
    setState(() {
      _isAvailable = isAvailable;
      _products = productDetailResponse.productDetails;
      _notFoundIds = productDetailResponse.notFoundIDs;
      _consumables = consumables;
      _purchasePending = false;
      _loading = false;
    });
  }

  @override
  void dispose() {
    _subscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final stack = <Widget>[];
    if (_queryProductError == null) {
      stack.add(
        ListView(
          children: <Widget>[
            _buildConnectionCheckTile(),
            _buildProductList(),
            _buildConsumableBox(),
            _buildRestoreButton(),
          ],
        ),
      );
    } else {
      stack.add(Center(child: Text(_queryProductError!)));
    }
    if (_purchasePending) {
      stack.add(
        const Stack(
          children: <Widget>[
            Opacity(
              opacity: 0.3,
              child: ModalBarrier(dismissible: false, color: Colors.grey),
            ),
            Center(child: CircularProgressIndicator()),
          ],
        ),
      );
    }

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('IAP Example')),
        body: Stack(children: stack),
      ),
    );
  }

  Card _buildConnectionCheckTile() {
    if (_loading) {
      return const Card(child: ListTile(title: Text('Trying to connect...')));
    }
    final Widget storeHeader = ListTile(
      leading: Icon(
        _isAvailable ? Icons.check : Icons.block,
        color: _isAvailable
            ? Colors.green
            : ThemeData.light().colorScheme.error,
      ),
      title: Text(
        'The store is ${_isAvailable ? 'available' : 'unavailable'}.',
      ),
    );
    final children = <Widget>[storeHeader];

    if (!_isAvailable) {
      children.addAll(<Widget>[
        const Divider(),
        ListTile(
          title: Text(
            'Not connected',
            style: TextStyle(color: ThemeData.light().colorScheme.error),
          ),
          subtitle: const Text(
            'Unable to connect to the payments processor. Has this app been configured correctly? See the example README for instructions.',
          ),
        ),
      ]);
    }
    return Card(child: Column(children: children));
  }

  Widget _buildProductList() {
    if (_loading) {
      return const Card(
        child: ListTile(
          leading: CircularProgressIndicator(),
          title: Text('Fetching products...'),
        ),
      );
    }
    if (!_isAvailable) {
      return const Card();
    }
    const productHeader = ListTile(
      title: Text(
        'Products for Sale',
        style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
      ),
    );
    const promoHeader = ListTile(
      title: Text(
        'Products in promo',
        style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
      ),
    );
    final productList = <ListTile>[];
    if (_notFoundIds.isNotEmpty) {
      productList.add(
        ListTile(
          title: Text(
            '[${_notFoundIds.join(", ")}] not found',
            style: TextStyle(color: ThemeData.light().colorScheme.error),
          ),
          subtitle: const Text(
            'This app needs special configuration to run. Please see example/README.md for instructions.',
          ),
        ),
      );
    }

    // This loading previous purchases code is just a demo. Please do not use this as it is.
    // In your app you should always verify the purchase data using the `verificationData` inside the [PurchaseDetails] object before trusting it.
    // We recommend that you use your own server to verify the purchase data.
    final purchases = Map<String, PurchaseDetails>.fromEntries(
      _purchases.map((PurchaseDetails purchase) {
        if (purchase.pendingCompletePurchase) {
          _iapStoreKitPlatform.completePurchase(purchase);
        }
        return MapEntry<String, PurchaseDetails>(purchase.productID, purchase);
      }),
    );
    productList.addAll(
      _products.map((ProductDetails productDetails) {
        final PurchaseDetails? previousPurchase = purchases[productDetails.id];
        return ListTile(
          title: Text(productDetails.title),
          subtitle: Text(productDetails.description),
          trailing: previousPurchase != null
              ? IconButton(
                  onPressed: () {
                    _iapStoreKitPlatformAddition.showPriceConsentIfNeeded();
                  },
                  icon: const Icon(Icons.upgrade),
                )
              : TextButton(
                  style: TextButton.styleFrom(
                    backgroundColor: Colors.green[800],
                    foregroundColor: Colors.white,
                  ),
                  onPressed: () {
                    final purchaseParam = PurchaseParam(
                      productDetails: productDetails,
                    );
                    if (productDetails.id == _kConsumableId) {
                      _iapStoreKitPlatform.buyConsumable(
                        purchaseParam: purchaseParam,
                      );
                    } else {
                      _iapStoreKitPlatform.buyNonConsumable(
                        purchaseParam: purchaseParam,
                      );
                    }
                  },
                  child: Text(productDetails.price),
                ),
        );
      }),
    );

    return Column(
      children: <Widget>[
        Card(
          child: Column(
            children: <Widget>[productHeader, const Divider(), ...productList],
          ),
        ),
        Card(
          child: Column(
            children: <Widget>[
              promoHeader,
              const Divider(),
              FutureBuilder<List<ListTile>>(
                future: _buildPromoList(),
                builder:
                    (
                      BuildContext context,
                      AsyncSnapshot<List<ListTile>> snapshot,
                    ) {
                      final List<ListTile>? data = snapshot.data;

                      if (data != null) {
                        return Column(children: data);
                      }

                      return const SizedBox.shrink();
                    },
              ),
            ],
          ),
        ),
      ],
    );
  }

  Future<List<ListTile>> _buildPromoList() async {
    final promoList = <ListTile>[];
    for (final ProductDetails detail in _products) {
      if (detail is AppStoreProduct2Details) {
        final SK2SubscriptionInfo? subscription =
            detail.sk2Product.subscription;
        final List<SK2SubscriptionOffer> offers =
            subscription?.promotionalOffers ?? <SK2SubscriptionOffer>[];

        for (final offer in offers) {
          if (offer.type == SK2SubscriptionOfferType.winBack) {
            final bool eligible = await _iapStoreKitPlatform
                .isWinBackOfferEligible(detail.id, offer.id ?? '');

            if (!eligible) {
              continue;
            }
          }

          promoList.add(_buildPromoTile(detail, offer));
        }
      }
    }
    return promoList;
  }

  ListTile _buildPromoTile(
    ProductDetails productDetails,
    SK2SubscriptionOffer offer,
  ) {
    return ListTile(
      title: Text('${productDetails.title} [${offer.type.name}]'),
      subtitle: Text(productDetails.description),
      trailing: TextButton(
        style: TextButton.styleFrom(
          backgroundColor: Colors.green[800],
          foregroundColor: Colors.white,
        ),
        onPressed: () {
          final purchaseParam = Sk2PurchaseParam.fromOffer(
            productDetails: productDetails,
            offer: offer,
            signature: SK2SubscriptionOfferSignature(
              keyID: 'keyID',
              nonce: '1ac96421-947d-45e9-a0a0-5bb3a6937284',
              timestamp: DateTime.now().millisecondsSinceEpoch,
              signature: 'dmFsaWRzaWduYXR1cmU=',
            ),
          );

          _iapStoreKitPlatform.buyNonConsumable(purchaseParam: purchaseParam);
        },
        child: Text('${productDetails.currencySymbol} ${offer.price}'),
      ),
    );
  }

  Card _buildConsumableBox() {
    if (_loading) {
      return const Card(
        child: ListTile(
          leading: CircularProgressIndicator(),
          title: Text('Fetching consumables...'),
        ),
      );
    }
    if (!_isAvailable || _notFoundIds.contains(_kConsumableId)) {
      return const Card();
    }
    const consumableHeader = ListTile(title: Text('Purchased consumables'));
    final List<Widget> tokens = _consumables.map((String id) {
      return GridTile(
        child: IconButton(
          icon: const Icon(Icons.stars, size: 42.0, color: Colors.orange),
          splashColor: Colors.yellowAccent,
          onPressed: () => consume(id),
        ),
      );
    }).toList();
    return Card(
      child: Column(
        children: <Widget>[
          consumableHeader,
          const Divider(),
          GridView.count(
            crossAxisCount: 5,
            shrinkWrap: true,
            padding: const EdgeInsets.all(16.0),
            children: tokens,
          ),
        ],
      ),
    );
  }

  Widget _buildRestoreButton() {
    if (_loading) {
      return Container();
    }

    return Padding(
      padding: const EdgeInsets.all(4.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          TextButton(
            style: TextButton.styleFrom(
              backgroundColor: Theme.of(context).colorScheme.primary,
              foregroundColor: Colors.white,
            ),
            onPressed: () => _iapStoreKitPlatform.restorePurchases(),
            child: const Text('Restore purchases'),
          ),
        ],
      ),
    );
  }

  Future<void> consume(String id) async {
    await ConsumableStore.consume(id);
    final List<String> consumables = await ConsumableStore.load();
    setState(() {
      _consumables = consumables;
    });
  }

  void showPendingUI() {
    setState(() {
      _purchasePending = true;
    });
  }

  Future<void> deliverProduct(PurchaseDetails purchaseDetails) async {
    // IMPORTANT!! Always verify purchase details before delivering the product.
    if (purchaseDetails.productID == _kConsumableId) {
      await ConsumableStore.save(purchaseDetails.purchaseID!);
      final List<String> consumables = await ConsumableStore.load();
      setState(() {
        _purchasePending = false;
        _consumables = consumables;
      });
    } else {
      setState(() {
        _purchases.add(purchaseDetails);
        _purchasePending = false;
      });
    }
  }

  void handleError(IAPError error) {
    setState(() {
      _purchasePending = false;
    });
  }

  Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) {
    // IMPORTANT!! Always verify a purchase before delivering the product.
    // For the purpose of an example, we directly return true.
    return Future<bool>.value(true);
  }

  void _handleInvalidPurchase(PurchaseDetails purchaseDetails) {
    // handle invalid purchase here if  _verifyPurchase` failed.
  }

  void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
    purchaseDetailsList.forEach(_handleReportedPurchaseState);
  }

  Future<void> _handleReportedPurchaseState(
    PurchaseDetails purchaseDetails,
  ) async {
    if (purchaseDetails.status == PurchaseStatus.pending) {
      showPendingUI();
    } else {
      if (purchaseDetails.status == PurchaseStatus.error) {
        handleError(purchaseDetails.error!);
      } else if (purchaseDetails.status == PurchaseStatus.purchased ||
          purchaseDetails.status == PurchaseStatus.restored) {
        final bool valid = await _verifyPurchase(purchaseDetails);
        if (valid) {
          await deliverProduct(purchaseDetails);
        } else {
          _handleInvalidPurchase(purchaseDetails);
          return;
        }
      }

      if (purchaseDetails.pendingCompletePurchase) {
        await _iapStoreKitPlatform.completePurchase(purchaseDetails);
      }
    }
  }
}
